/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
import {appContext} from '../../../services/app-context.js';

const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
const MAX_GET_TOKEN_RETRIES = 2;

/**
 * Auth class.
 */
export class Auth {
  // TODO(taoalpha): this whole thing should be moved to a service

  constructor() {
    this._type = null;
    this._cachedTokenPromise = null;
    this._defaultOptions = {};
    this._retriesLeft = MAX_GET_TOKEN_RETRIES;
    this._status = Auth.STATUS.UNDETERMINED;
    this._authCheckPromise = null;
    this._last_auth_check_time = Date.now();
    this.eventEmitter = appContext.eventEmitter;
  }

  get baseUrl() {
    return BaseUrlBehavior.getBaseUrl();
  }

  /**
   * Returns if user is authed or not.
   *
   * @returns {!Promise<boolean>}
   */
  authCheck() {
    if (!this._authCheckPromise ||
      (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
    ) {
      // Refetch after last check expired
      this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
      this._last_auth_check_time = Date.now();
    }

    return this._authCheckPromise.then(res => {
      // auth-check will return 204 if authed
      // treat the rest as unauthed
      if (res.status === 204) {
        this._setStatus(Auth.STATUS.AUTHED);
        return true;
      } else {
        this._setStatus(Auth.STATUS.NOT_AUTHED);
        return false;
      }
    }).catch(e => {
      this._setStatus(Auth.STATUS.ERROR);
      // Reset _authCheckPromise to avoid caching the failed promise
      this._authCheckPromise = null;
      return false;
    });
  }

  clearCache() {
    this._authCheckPromise = null;
  }

  /**
   * @param {Auth.STATUS} status
   */
  _setStatus(status) {
    if (this._status === status) return;

    if (this._status === Auth.STATUS.AUTHED) {
      this.eventEmitter.emit('auth-error', {
        message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials',
      });
    }
    this._status = status;
  }

  get status() {
    return this._status;
  }

  get isAuthed() {
    return this._status === Auth.STATUS.AUTHED;
  }

  _getToken() {
    return Promise.resolve(this._cachedTokenPromise);
  }

  /**
   * Enable cross-domain authentication using OAuth access token.
   *
   * @param {
   *   function(): !Promise<{
   *     access_token: string,
   *     expires_at: number
   *   }>
   * } getToken
   * @param {?{credentials:string}} defaultOptions
   */
  setup(getToken, defaultOptions) {
    this._retriesLeft = MAX_GET_TOKEN_RETRIES;
    if (getToken) {
      this._type = Auth.TYPE.ACCESS_TOKEN;
      this._cachedTokenPromise = null;
      this._getToken = getToken;
    }
    this._defaultOptions = {};
    if (defaultOptions) {
      for (const p of ['credentials']) {
        this._defaultOptions[p] = defaultOptions[p];
      }
    }
  }

  /**
   * Perform network fetch with authentication.
   *
   * @param {string} url
   * @param {Object=} opt_options
   * @return {!Promise<!Response>}
   */
  fetch(url, opt_options) {
    const options = Object.assign({
      headers: new Headers(),
    }, this._defaultOptions, opt_options);
    if (this._type === Auth.TYPE.ACCESS_TOKEN) {
      return this._getAccessToken().then(
          accessToken =>
            this._fetchWithAccessToken(url, options, accessToken)
      );
    } else {
      return this._fetchWithXsrfToken(url, options);
    }
  }

  _getCookie(name) {
    const key = name + '=';
    let result = '';
    document.cookie.split(';').some(c => {
      c = c.trim();
      if (c.startsWith(key)) {
        result = c.substring(key.length);
        return true;
      }
    });
    return result;
  }

  _isTokenValid(token) {
    if (!token) { return false; }
    if (!token.access_token || !token.expires_at) { return false; }

    const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
    if (Date.now() >= expiration.getTime()) { return false; }

    return true;
  }

  _fetchWithXsrfToken(url, options) {
    if (options.method && options.method !== 'GET') {
      const token = this._getCookie('XSRF_TOKEN');
      if (token) {
        options.headers.append('X-Gerrit-Auth', token);
      }
    }
    options.credentials = 'same-origin';
    return fetch(url, options);
  }

  /**
   * @return {!Promise<string>}
   */
  _getAccessToken() {
    if (!this._cachedTokenPromise) {
      this._cachedTokenPromise = this._getToken();
    }
    return this._cachedTokenPromise.then(token => {
      if (this._isTokenValid(token)) {
        this._retriesLeft = MAX_GET_TOKEN_RETRIES;
        return token.access_token;
      }
      if (this._retriesLeft > 0) {
        this._retriesLeft--;
        this._cachedTokenPromise = null;
        return this._getAccessToken();
      }
      // Fall back to anonymous access.
      return null;
    });
  }

  _fetchWithAccessToken(url, options, accessToken) {
    const params = [];

    if (accessToken) {
      params.push(`access_token=${accessToken}`);
      const baseUrl = this.baseUrl;
      const pathname = baseUrl ?
        url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
      if (!pathname.startsWith('/a/')) {
        url = url.replace(pathname, '/a' + pathname);
      }
    }

    const method = options.method || 'GET';
    let contentType = options.headers.get('Content-Type');

    // For all requests with body, ensure json content type.
    if (!contentType && options.body) {
      contentType = 'application/json';
    }

    if (method !== 'GET') {
      options.method = 'POST';
      params.push(`$m=${method}`);
      // If a request is not GET, and does not have a body, ensure text/plain
      // content type.
      if (!contentType) {
        contentType = 'text/plain';
      }
    }

    if (contentType) {
      options.headers.set('Content-Type', 'text/plain');
      params.push(`$ct=${encodeURIComponent(contentType)}`);
    }

    if (params.length) {
      url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
    }
    return fetch(url, options);
  }
}

Auth.TYPE = {
  XSRF_TOKEN: 'xsrf_token',
  ACCESS_TOKEN: 'access_token',
};

/** @enum {number} */
Auth.STATUS = {
  UNDETERMINED: 0,
  AUTHED: 1,
  NOT_AUTHED: 2,
  ERROR: 3,
};

Auth.CREDS_EXPIRED_MSG = 'Credentials expired.';
// TODO(dmfilippov) move to appContext
export const authService = new Auth();

// TODO(dmfilippov) Remove the following lines with assignments
// Plugins can use global Auth because it was accessible with
// the global Gerrit... variable. To avoid breaking changes in plugins
// temporary assign global variables.
window.Gerrit = window.Gerrit || {};
window.Gerrit.Auth = authService;
